#include "channel.h"

/**
 * Returns the index of the available device if it is accessible and if it has some important features, e. . enough memory for executing the program
 * @param memory_required required amount of memory in Megabytes (you can use the macro MEGA for finding that value)
 * @return index of the device or -1 if problems occour
 */

int getPhysicalDevice (float memory_required) {

    cudaError_t error;
    struct cudaDeviceProp deviceProp;
    int num_device;
    float memory_amount;

    /* "rough" GPU check */
    if ( (error = cudaGetDeviceCount (&num_device)) != cudaSuccess) {
        printf ("%s\n", cudaGetErrorString(error));
        return -1;
    }
    
    if (num_device == 0) {
        printf ("There is no device supporting CUDA\n");
        return -1;
    }
    
    /* take the first device (no multiGPU support because we have dependency on the previous result) */
    if ( (error =cudaGetDeviceProperties (&deviceProp, FIRSTDEVICE)) != cudaSuccess) {
        printf ("%s\n", cudaGetErrorString(error));
        return -1;
    }
    
    memory_amount = deviceProp.totalGlobalMem / MEGA;
    
    printf("\nTotal amount of global memory: %.0f MBytes\n", memory_amount);
    printf("Total amount of required memory: %.0f MBytes\n\n", memory_required);
    
    if (memory_amount <= (memory_required + DELTA)) {
        printf ("Memory not sufficient\n");
        return -1;
    }
    
    return FIRSTDEVICE;
}

/**
 * Creates a channel toward the device for submitting data of dimension widthxheigth
 * @param device index of the device
 * @param width width of the matrixes
 * @param heigth heigth of the matrixes
 * @return the pointer to the channel in case of success, or NULL
 */

Channel* createVirtualChannel (int device, int width, int heigth) {

    cudaError_t error;
    culaStatus s;
    Channel* ch;
    
    /* allocate the host memory for the channel */
    if ((ch = (Channel*) malloc (sizeof (Channel))) == NULL) {
        printf ("No possible to allocate the channel\n");
        return NULL;
    }
    
    /* initialize the channel descriptor */
    ch->ins = ch->estr = 0;
    ch->frees = SIZE_CH;
    ch->throwed = FALSE;
    ch->matrix_width = width;
    ch->matrix_dim = width * width;
    
    /* initialize the CUDA device */
    if ( (error = cudaSetDevice (device)) != cudaSuccess) {
        printf ("No possible to initialize the CUDA environment because %s\n", cudaGetErrorString(error));
        return NULL;
    }
    
    if ( (error = cudaEventCreateWithFlags (&(ch->finish), cudaEventBlockingSync)) != cudaSuccess) {
        printf ("Impossible to create the channel because %s\n", cudaGetErrorString(error));
        return NULL;
    }
    
    /* allocate data structures on GPU memory */
    if ( (error = cudaMalloc ( (void**) &(ch->buffer_0), ch->matrix_dim * sizeof (unsigned char))) != cudaSuccess) {
        printf ("No possible to allocate the memory because %s, if possible TRY TO REDUCE THE RESOLUTION of the screen and execute again\n", cudaGetErrorString(error));
        return NULL;
    }
    if ( (error = cudaMalloc ( (void**) &(ch->buffer_1), ch->matrix_dim * sizeof (unsigned char))) != cudaSuccess) {
        printf ("No possible to allocate the memory because %s, if possible TRY TO REDUCE THE RESOLUTION of the screen and execute again\n", cudaGetErrorString(error));
        return NULL;
    }
    if ( (error = cudaMalloc ( (void**) &(ch->matrix), ch->matrix_dim * 8 * sizeof (float))) != cudaSuccess) {
        printf ("No possible to allocate the memory because %s, if possible TRY TO REDUCE THE RESOLUTION of the screen and execute again\n", cudaGetErrorString(error));
        return NULL;
    }
    if ( (error = cudaMalloc ( (void**) &(ch->vector), ch->matrix_dim * sizeof (float))) != cudaSuccess) {
        printf ("No possible to allocate the memory because %s, if possible TRY TO REDUCE THE RESOLUTION of the screen and execute again\n", cudaGetErrorString(error));
        return NULL;
    }
    if ( (error = cudaMalloc ( (void**) &(ch->gradient_matrix), 8 * 8 * sizeof (float))) != cudaSuccess) {
        printf ("No possible to allocate the memory because %s, if possible TRY TO REDUCE THE RESOLUTION of the screen and execute again\n", cudaGetErrorString(error));
        return NULL;
    }
    if ( (error = cudaMalloc ( (void**) &(ch->gradient_vector), 8 * sizeof (float))) != cudaSuccess) {
        printf ("No possible to allocate the memory because %s, if possible TRY TO REDUCE THE RESOLUTION of the screen and execute again\n", cudaGetErrorString(error));
        return NULL;
    }
    if ( (error = cudaMalloc ( (void**) &(ch->gaussian_par), 8 * sizeof (float))) != cudaSuccess) {
        printf ("No possible to allocate the memory because %s, if possible TRY TO REDUCE THE RESOLUTION of the screen and execute again\n", cudaGetErrorString(error));
        return NULL;
    }
    if ( (error = cudaMalloc ( (void**) &(ch->pivot_vector), 8 * sizeof (int))) != cudaSuccess) {
        printf ("No possible to allocate the memory because %s, if possible TRY TO REDUCE THE RESOLUTION of the screen and execute again\n", cudaGetErrorString(error));
        return NULL;
    }
    
    /* initialize the CULA environment */
    if ( ( s = culaSelectDevice (device)) != culaNoError) {
        printf ("No possible to initialize the CULA environment because %s\n", culaGetStatusString(s));
        return NULL;
    }
    
    if ( (s = culaInitialize()) != culaNoError) {
        printf ("No possible to initialize the CULA environment because %s\n", culaGetStatusString(s));
        return NULL;
    }
    
    return ch;
}

/**
 * Initialize the channel in input with the parameters for computing the algorithm. This is necessary only the first time because the Gaussian parameters during the execution are extimated and updated by the support.
 * @param ch virtual channel
 * @param parameters Gaussian parameters
 * @return 0 in case of success, -1 otherwise
 */

int initializeVirtualChannel (Channel* ch, float* parameters) {
    
    cudaError_t error;
    int i;
    int pivot [8];
    
    /* inizialization gaussian parameters */    
    if ( (error = cudaMemcpy (ch->gaussian_par, parameters, 8 * sizeof (float), cudaMemcpyHostToDevice)) != cudaSuccess) {
        printf ("Impossible to copy parameters because %s\n", cudaGetErrorString(error));
        return -1;
    }
    
    /* inizialization pivot array */
    for (i = 0; i < 8; ++i)
        pivot[i] = 0;
    if ( (error = cudaMemcpy (ch->pivot_vector, pivot, 8 * sizeof (int), cudaMemcpyHostToDevice)) != cudaSuccess) {
        printf ("Impossible to copy parameters because %s\n", cudaGetErrorString(error));
        return -1;
    }
    
    return 0;
}

/**
 * Deallocates the memory due to the implementation of the channel.
 * @param ch channel to destroy
 * @return 0 in case of success, -1 otherwise
 */

int destroyVirtualChannel (Channel* ch) {
    
    cudaError_t error;
    
    /* deallocate GPU memory */
    if ( (error = cudaFree (ch->buffer_0)) != cudaSuccess) {
        printf ("No possible to deallocate the memory because %s\n", cudaGetErrorString(error));
        return -1;
    }
    if ( (error = cudaFree (ch->buffer_1)) != cudaSuccess) {
        printf ("No possible to deallocate the memory because %s\n", cudaGetErrorString(error));
        return -1;
    }
    if ( (error = cudaFree (ch->matrix)) != cudaSuccess) {
        printf ("No possible to deallocate the memory because %s\n", cudaGetErrorString(error));
        return -1;
    }
    if ( (error = cudaFree (ch->vector)) != cudaSuccess) {
        printf ("No possible to deallocate the memory because %s\n", cudaGetErrorString(error));
        return -1;
    }
    if ( (error = cudaFree (ch->gradient_matrix)) != cudaSuccess) {
        printf ("No possible to deallocate the memory because %s\n", cudaGetErrorString(error));
        return -1;
    }
    if ( (error = cudaFree (ch->gradient_vector)) != cudaSuccess) {
        printf ("No possible to deallocate the memory because %s\n", cudaGetErrorString(error));
        return -1;
    }
    if ( (error = cudaFree (ch->gaussian_par)) != cudaSuccess) {
        printf ("No possible to deallocate the memory because %s\n", cudaGetErrorString(error));
        return -1;
    }
    if ( (error = cudaFree (ch->pivot_vector)) != cudaSuccess) {
        printf ("No possible to deallocate the memory because %s\n", cudaGetErrorString(error));
        return -1;
    }
    
    /* deallocate host memory */
    free (ch);
    
    /* destroy the CULA envornment */
    culaShutdown();
    
    return 0;
}

/****************************************************************************
 * Definitions of wrapper for CUDA kernes invocation in the submit operation*
 ****************************************************************************/

void computeVectorWrapper (unsigned char* image, float* vector, float* parameters, int width, int dim);

void computeMatrixWrapper (float* matrix, float* parameters, int width, int dim);

void updateParametersWrapper (float* delta, float* parameters);

/**
 * Submits a matrix to compute into the channel. This is a BLOCKING function: in case the buffer in the channel is full up the host process waits in an active mode.
 * @param ch channel toward a device
 * @param matrix data to compute
 * @return 0 in case of success, -1 otherwise
 */

int submit (Channel* ch, unsigned char* matrix) {
    
    cudaError_t error;
    
    /* buffer is full */
    if(ch->frees == 0) {
        
#ifdef DEBUG
        printf ("buffer pieno\n");
#endif
        /* we WAIT for the event */
        if ( (error = cudaEventSynchronize (ch->finish)) != cudaSuccess) {
            printf ("Impossible to synchronize because %s\n", cudaGetErrorString(error));
            return -1;
        }
        /* we update the channel */
        ++(ch->frees);
        ch->estr = (ch->estr + 1) % SIZE_CH;
        
        /* WORK */
        
        /* compute the gradient vector and matrix */
        if (ch->estr == 0) {
            computeVectorWrapper (ch->buffer_0, ch->vector, ch->gaussian_par, ch->matrix_width, ch->matrix_dim);
            computeMatrixWrapper (ch->matrix, ch->gaussian_par, ch->matrix_width, ch->matrix_dim);
        } else {
            computeVectorWrapper (ch->buffer_1, ch->vector, ch->gaussian_par, ch->matrix_width, ch->matrix_dim);
            computeMatrixWrapper (ch->matrix, ch->gaussian_par, ch->matrix_width, ch->matrix_dim);
        }
        
        /* multiplications */
        if (culaDeviceSgemv ('N', 8, ch->matrix_dim, (float) 1.0, ch->matrix, 8, ch->vector, 1, 0, ch->gradient_vector, 1) != culaNoError) {
            printf ("Error executing the algorithm\n");
            return -1;
        }
        
        if (culaDeviceSgemm ('N', 'T', 8, 8, ch->matrix_dim, (float) 1.0, ch->matrix, 8, ch->matrix, 8, 0, ch->gradient_matrix , 8) != culaNoError) {
            printf ("Error executing the algorithm\n");
            return -1;
        }
        
        /* solve the linear system */
        if (culaDeviceSgesv (8, 1, ch->gradient_matrix, 8, ch->pivot_vector, ch->gradient_vector, 8) != culaNoError) {
            printf ("Error executing the algorithm\n");
            return -1;
        }
        
        /* update the result */
        updateParametersWrapper (ch->gradient_vector, ch->gaussian_par);
        
        /* update the channel*/
        if ( (error = cudaEventRecord (ch->finish, 0)) != cudaSuccess) {
            printf ("Impossible to record the event because %s\n", cudaGetErrorString(error));
            return -1;
        }
        
    }
    
    /* buffer has free locations so I copy the image in the right place ... */
    if (ch->ins == 0) {
        if ( (error = cudaMemcpyAsync (ch->buffer_0, matrix, ch->matrix_dim * sizeof (unsigned char), cudaMemcpyHostToDevice, 0)) != cudaSuccess) {
            printf ("Impossible to copy the image because %s\n", cudaGetErrorString(error));
            return -1;
        }
    } else {
        if ( (error = cudaMemcpyAsync (ch->buffer_1, matrix, ch->matrix_dim * sizeof (unsigned char), cudaMemcpyHostToDevice, 0)) != cudaSuccess) {
            printf ("Impossible to copy the image because %s\n", cudaGetErrorString(error));
            return -1;
        }
    }
    
    /* ... and update the state of the channel */
    ch->ins = (ch->ins + 1) % SIZE_CH;
    --(ch->frees);
    
    /* ask for the event: if GPU is not working we modify the status and submits a task */
    if ( (error = cudaEventQuery (ch->finish)) == cudaSuccess) {
        
        if (ch->throwed == TRUE) {
            ++(ch->frees);
            ch->estr = (ch->estr + 1) % SIZE_CH;
            ch->throwed = FALSE;
        }
        
        /* WORK */
        
        if (ch->estr == 0) {
            computeVectorWrapper (ch->buffer_0, ch->vector, ch->gaussian_par, ch->matrix_width, ch->matrix_dim);
            computeMatrixWrapper (ch->matrix, ch->gaussian_par, ch->matrix_width, ch->matrix_dim);
        } else {
            computeVectorWrapper (ch->buffer_1, ch->vector, ch->gaussian_par, ch->matrix_width, ch->matrix_dim);
            computeMatrixWrapper (ch->matrix, ch->gaussian_par, ch->matrix_width, ch->matrix_dim);
        }
        
        /* multiplications */
        if (culaDeviceSgemv ('N', 8, ch->matrix_dim, (float) 1.0, ch->matrix, 8, ch->vector, 1, 0, ch->gradient_vector, 1) != culaNoError) {
            printf ("Error executing the algorithm\n");
            return -1;
        }
        
        if (culaDeviceSgemm ('N', 'T', 8, 8, ch->matrix_dim, (float) 1.0, ch->matrix, 8, ch->matrix, 8, 0, ch->gradient_matrix , 8) != culaNoError) {
            printf ("Error executing the algorithm\n");
            return -1;
        }
        
        /* solve the linear system */
        if (culaDeviceSgesv (8, 1, ch->gradient_matrix, 8, ch->pivot_vector, ch->gradient_vector, 8) != culaNoError) {
            printf ("Error executing the algorithm\n");
            return -1;
        }
        
        /* update the result */
        updateParametersWrapper (ch->gradient_vector, ch->gaussian_par);
        
        /* update the channel*/
        if ( (error = cudaEventRecord (ch->finish, 0)) != cudaSuccess) {
            printf ("Impossible to record the event because %s\n", cudaGetErrorString(error));
            return -1;
        }
        ch->throwed = TRUE;
    
    } else {
        if (error != cudaErrorNotReady) {
            printf ("Impossible to query the event because %s\n", cudaGetErrorString(error));
            return -1;
        }
    }
    
    return 0;
}

/**
 * Permits to retrieve the result that are stored in gaussian_parameters
 * @return 0 in case of success, -1 otherwise
 */

int getResult (Channel* ch, float* gaussian_parameters) {
    
    cudaError_t error;
    
    if ( (error = cudaMemcpy (gaussian_parameters, ch->gaussian_par, 8 * sizeof (float), cudaMemcpyDeviceToHost)) != cudaSuccess) {
        printf ("Impossible to copy parameters because %s\n", cudaGetErrorString(error));
        return -1;
    }
    
    return 0;
}
